Skip to content

Migrate to Hydrogen 3 (storefront-kit working version, Remix → React Router 7) - #154

Draft
lucyxiang wants to merge 6 commits into
Shopify:mainfrom
lucyxiang:migrate-hydrogen-3-preview
Draft

Migrate to Hydrogen 3 (storefront-kit working version, Remix → React Router 7)#154
lucyxiang wants to merge 6 commits into
Shopify:mainfrom
lucyxiang:migrate-hydrogen-3-preview

Conversation

@lucyxiang

@lucyxiang lucyxiang commented Jun 30, 2026

Copy link
Copy Markdown

What

Migrates the demo store from Remix to React Router 7 on Hydrogen 3, now targeting the storefront-kit working version Hydrogen 3 is consumed as a server-handler library you compose, not a framework that owns the request.

  • Vendored package: @shopify/hydrogen@0.0.1 is unpublished (storefront-kit workspace pkg), so it is packed to vendor/shopify-hydrogen-0.0.1.tgz and installed via a file: dependency.
  • CLI dev restored: shopify hydrogen dev|build work again. The hydrogen() Vite plugin now ships in @shopify/hydrogen-classic/vite (@shopify/hydrogen-classic = npm:@shopify/hydrogen@2026.4.2); the CLI injects .env into MiniOxygen and serves /graphiql + /subrequest-profiler.
  • Server-handler wiring (app/storefront.middleware.ts): createShopifyRequestContextcreateStorefrontClient (private) → handleShopifyRoutes with cartHandlershandleShopifyRedirects on 404, plus the customer-account client and session commit.
  • Customer account (app/lib/customer-account.server.ts): a compat wrapper over @shopify/hydrogen/customer-account (createCustomerSessionprepareLoginUrl/handleOAuthCallback/logout), replacing the hand-rolled OAuth URL.
  • Version set aligned to storefront-kit: react-router/@react-router/* 7.15.1, @shopify/cli 3.94.3, @shopify/mini-oxygen 4.1.0, vite ^8, React 18.3.1.

What the initial (preview) migration did NOT get right vs production

The first pass (npm 0.0.0-preview build) shipped several regressions against prod (hydrogen.shop). Status after this re-migration:

# Initial-migration defect vs prod Root cause Status now
1 /account/login returned 400 Hand-rolled OAuth URL used the wrong path…/auth/oauth/authorize instead of …/oauth/authorize. (The client_id was correct all along — the raw PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID UUID, not a shp_ value; that earlier theory was wrong.) Fixed — URL now built by @shopify/hydrogen/customer-account. Verified the constructed URL (path, redirect_uri, scope, PKCE). Local login requires an HTTPS tunnel (library refuses http origins by design); works on prod https.
2 Add-to-cart worked but the drawer never opened (and the quantity control looked off) The client uses the library useCartForm(), which submits through its own cart store (store.handleFormSubmit + preventDefault), so React Router's useFetchers() never sees the request — the ported useCartFetchers drawer trigger could never fire. The quantity field also rendered the interactive <input>'s native chrome. FixedAddToCartButton rewritten on useCartForm(); the drawer now opens off the cart store via useCart((s) => s.pending.lines.size > 0) (a mutation signal, not absolute totalQuantity, which would auto-open on every page load). Quantity control restyled to match prod. /api/cart verified: POST intent=add → 303 + CartCreate + cart cookie; GET returns totalQuantity:1.
3 /collections/$handle pagination: "Next" button vs prod's "Load more" infinite-append Hydrogen 3 removed <Pagination> + getPaginationVariables (confirmed still absent in 0.0.1); needs hand-rolled accumulate + IntersectionObserver ⚠️ Open — re-verified (2026-07-01): /collections/freestyle dev renders 12 products + "Next" (height 1926px) vs prod's full grid + "Load more products" (height 3684px). /products (=/collections/all) matches. This is the one remaining browse regression.
4 404 page missing the "Featured Products" swimlane; page never reached network-idle Deferred (defer/<Await>) featured-data loader appeared to hang after the RR7 migration Resolved — re-verified (2026-07-01): dev's /notfound now renders the Featured Products swimlane pixel-identical to prod (heights match, 1451px). The deferred <Await> path works on this build.

Install notes

  • Use npm, not pnpm. pnpm symlinks @shopify/hydrogen into .pnpm/, which defeats the Shopify CLI's realpath-based Hydrogen-project detection ("…outside of a Hydrogen project"). npm's flat install keeps it a real dir under the project root.
  • legacy-peer-deps=true in .npmrc@shopify/hydrogen-classic peers an exact @react-router/dev that differs from the app's pin.

Functional parity vs the previous store

This is a like-for-like port, not a feature addition — there is no net-new user-facing functionality:

  • Route set is identical (32 route files before and after; none added or removed).
  • Capabilities that look new already existed in the Hydrogen 2 store: OAuth customer login, CSP nonces, and Shopify analytics.
  • The ~20 "new" files (Image, Money, MediaFile, Pagination, flatten-connection, csp/nonce, analytics, customer-account.server, …) are local reimplementations of things @shopify/hydrogen used to provide, because Hydrogen 3 is a slim server-handler library rather than a batteries-included framework. Pagination is currently a regression (see item 3 above), not a new feature.

Post-migration improvements (this PR)

Small, functionality-preserving polish surfaced by comparing against the storefront-kit examples/react-router template:

  • Typed GraphQL was silently off. tsconfig pointed the gql.tada plugin at a nonexistent src/graphql/… schema path. Repointed to the packaged dist/*.schema.json under named storefront + customer-account schemas, and added gql.tada check to typecheck so query/field type errors fail the build.
  • Accessibility. The skip-to-content link was permanently sr-only (unusable by keyboard) — now reveals on focus; #mainContent is focused on route change; the cart trigger got an accessible name + aria-haspopup.
  • Local customer login. Added dev:customer (shopify hydrogen dev --customer-account-push) so the Customer Account OAuth flow works locally over the CLI tunnel (satisfies the HTTPS-origin requirement).
  • Dead code. Removed the orphaned useCartFetchers hook (unused after the cart-drawer fix).

Notes

  • .env is not committed with private tokens (shpat_ / PRIVATE_STOREFRONT_API_TOKEN stay local; only PUBLIC_* + SESSION_SECRET + SHOP_ID are tracked, pre-existing).
  • Route-by-route pixel comparison re-run (2026-07-01) across home, collections, collection, products-all, product, journal, 404. All layouts match prod except item 3; item 4 now resolved. Remaining diff is the /collections/$handle load-more pagination (item 3); draft until that reaches parity.

🤖 Generated with Claude Code

lucyxiang and others added 2 commits June 30, 2026 19:41
Migrate the demo store from Remix to React Router 7.16 on the Hydrogen 3
preview (0.0.0-preview-0c3bff8-20260618001533): fs-routes, middleware-based
storefront/customer-account context, and Oxygen via @shopify/mini-oxygen.

Dev-enablement fixes:
- vite.config.ts: inject .env into the Oxygen worker via
  oxygen({env: loadEnv(...)}) and pre-bundle react/react-dom for SSR
  (the preview build ships no hydrogen() Vite plugin / no /vite export)
- .nvmrc: v18 → v22.18.0 (React Router 7 requires Node >20)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves off the npm preview build (0.0.0-preview) to the storefront-kit
working version of Hydrogen 3, vendored as @shopify/hydrogen@0.0.1
(vendor/shopify-hydrogen-0.0.1.tgz, file: dependency).

- package.json: shopify hydrogen dev|build restored; @shopify/hydrogen-classic
  (npm:@shopify/hydrogen@2026.4.2) supplies the hydrogen() vite plugin;
  react-router/@react-router/* 7.15.1, @shopify/cli 3.94.3, mini-oxygen 4.1.0.
- vite.config.ts: plugins [hydrogen(), oxygen(), reactRouter()] with hydrogen
  from @shopify/hydrogen-classic/vite; CLI injects env + worker entry.
- storefront.middleware.ts: server-handler wiring — createShopifyRequestContext,
  createStorefrontClient (private), handleShopifyRoutes with cartHandlers,
  handleShopifyRedirects, customer-account client + session commit.
- customer-account.server.ts: compat wrapper over @shopify/hydrogen/customer-account
  (prepareLoginUrl/handleOAuthCallback/logout) replacing the hand-rolled OAuth
  URL that produced the /account/login 400.
- customer-session.ts: vendored EncryptedCookieCustomerSession.
- .npmrc: legacy-peer-deps=true (hydrogen-classic peers exact @react-router/dev);
  install with npm, not pnpm (pnpm symlinks break CLI project detection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lucyxiang lucyxiang changed the title Migrate to Hydrogen 3 preview (Remix → React Router 7) Migrate to Hydrogen 3 (storefront-kit working version, Remix → React Router 7) Jul 1, 2026
lucyxiang and others added 3 commits June 30, 2026 23:32
.env is listed in .gitignore but was force-added upstream long ago, so it
stayed tracked. Untrack it on this branch so real tokens can't be committed
by accident. Local .env stays on disk for dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The library useCartForm submits through its own cart store
(store.handleFormSubmit + preventDefault), so React Router's
useFetchers never sees the request and the old useCartFetchers
drawer trigger never fired. Rewrite AddToCartButton to use
useCartForm and drive the drawer from the cart store.

Open the drawer on a mutation signal
(useCart((s) => s.pending.lines.size > 0)) rather than absolute
totalQuantity, which jumps 0 -> N on hydration and auto-opened
the drawer on every page load.

Match the quantity control to production: strip the interactive
input's native field chrome (appearance-none, border-0, no focus
ring) and tighten width so the -/+ layout matches the H2 store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix gql.tada schema config: pointed at a nonexistent
  src/graphql/... path so typed GraphQL was silently disabled.
  Use the packaged dist/*.schema.json under named storefront +
  customer-account schemas, and run gql.tada check in typecheck.
- Make the skip-to-content link visible on keyboard focus (was
  permanently sr-only) and focus #mainContent on route change.
- Give the cart trigger an accessible name + aria-haspopup.
- Add dev:customer script (shopify hydrogen dev
  --customer-account-push) so Customer Account OAuth works locally
  over the CLI tunnel.
- Remove orphaned useCartFetchers hook (dead after the cart-drawer
  fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lucyxiang
lucyxiang force-pushed the migrate-hydrogen-3-preview branch from 9593663 to 8c50e67 Compare July 1, 2026 04:36
…d of link navigation

Same-product option values now render as buttons wired to the library's
useProductForm register('optionValue') activation, updating the selected
variant immediately. Only cross-product values (different handle) keep
<Link> navigation. Addresses the laggy variant-change feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant